package jmine.tec.batch;

import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import jmine.tec.batch.dao.MPhaseDAO;
import jmine.tec.batch.domain.ExecPhase;
import jmine.tec.batch.domain.MPhase;
import jmine.tec.batch.domain.MProcess;
import jmine.tec.batch.domain.PhaseInstance;
import jmine.tec.batch.domain.Process;
import jmine.tec.batch.domain.ProcessingUnit;
import jmine.tec.batch.domain.enumx.PhaseStatusEnum;
import jmine.tec.batch.event.domain.Event;
import jmine.tec.batch.event.domain.enumx.EventActionModeEnum;
import jmine.tec.batch.event.domain.enumx.EventLevelEnum;
import jmine.tec.batch.graph.PhaseGraph;
import jmine.tec.persist.Versioned;
import jmine.tec.utils.debug.TimerGroup;
import jmine.tec.utils.debug.impl.DebugRuntimeFactory;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.StaleObjectStateException;
import org.hibernate.proxy.HibernateProxyHelper;

import bancosys.tec.exception.BusinessException;
import bancosys.tec.exception.LocalizedMessageHolder;
import bancosys.tec.exception.informer.UserFriendlyExceptionInformer;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.utils.date.Timestamp;

/**
 * The Phase Manager is the core of the processing framework. Given a process definition {@link MProcess} it determines which
 * {@link ExecPhase} should be executed and in which order respecting dependencies.
 * 
 * @param <E> the type of {@link PhaseableElement} this manager is going to handle
 * @author jrenaut
 */
public abstract class AbstractPhaseManager<E extends Persistable & Versioned> {

    public static final String PHASE_TIMER = "Phase Timer";

    /**
     * @return the {@link TimerGroup} for {@link ExecPhase} related readings.
     */
    public static TimerGroup getPhaseTimer() {
        return DebugRuntimeFactory.getInstance().getOrCreateTimer(PHASE_TIMER);
    }

    protected final UserFriendlyExceptionInformer userFriendlyExceptionInformer;

    protected final BatchController controller;

    protected final MProcess mprocess;

    private Map<E, Map<MPhase, PhaseInstance<E>>> phaseInstanceIndex;

    /**
     * C'tor
     * 
     * @param controller {@link KernelController}
     * @param mprocess {@link MProcess}
     * @param userFriendlyExceptionInformer {@link UserFriendlyExceptionInformer}
     */
    public AbstractPhaseManager(BatchController controller, MProcess mprocess, UserFriendlyExceptionInformer userFriendlyExceptionInformer) {
        this.controller = controller;
        this.mprocess = mprocess;
        this.userFriendlyExceptionInformer = userFriendlyExceptionInformer;
    }

    /**
     * Executes the {@link ExecPhase} related to the defined {@link MProcess} on all {@link PhaseableElement} of the given
     * {@link ProcessingUnit}.
     * 
     * @param unit {@link ProcessingUnit}
     */
    public void execute(final ProcessingUnit<E> unit) {
        List<MPhase> mphases = this.getPhases(this.mprocess);

        this.createPhaseInstances(unit, mphases);

        try {
            this.beforeUnit(unit);
            for (MPhase mphase : mphases) {
                ExecPhase<E> phase = this.getExecPhases(mphase);

                this.beforePhase(unit, phase);
                this.executePhase(unit, mphase, phase);
                this.afterPhase(unit, phase);
            }
            this.updateProcessingUnitStatusOnSuccess(unit);
        } catch (RuntimeException e) {
            this.updateProcessingUnitStatusOnFailure(unit);
            throw e;
        } finally {
            this.afterUnit(unit);
        }
    }

    /**
     * Updates the status of the processing unit to indicate that it failed.
     * 
     * @param unit the {@link ProcessingUnit}
     */
    protected void updateProcessingUnitStatusOnFailure(final ProcessingUnit<E> unit) {
        this.executeTransacted(new Callable<Void>() {
            public Void call() {
                AbstractPhaseManager.this.updateProcessingUnitStatus(unit, ProcessingUnit.ERROR);
                AbstractPhaseManager.this.updateProcessStatus(unit);
                return null;
            }
        });
    }

    /**
     * Updates the status of the processing unit to indicate that it succeeded.
     * 
     * @param unit the {@link ProcessingUnit}
     */
    protected void updateProcessingUnitStatusOnSuccess(final ProcessingUnit<E> unit) {
        this.executeTransacted(new Callable<Void>() {
            public Void call() {
                AbstractPhaseManager.this.updateProcessingUnitStatus(unit, ProcessingUnit.FINISHED);
                AbstractPhaseManager.this.updateProcessStatus(unit);
                return null;
            }
        });
    }

    /**
     * @param unit {@link ProcessingUnit}
     * @param status one of the statuses defined in {@link ProcessingUnit}
     */
    private void updateProcessingUnitStatus(ProcessingUnit<E> unit, int status) {
        unit.setStatus(status);
        unit.setFinishTime(new Timestamp());
        unit.getPersister().save();
    }

    /**
     * Updates the related {@link Process} in order to account for the status of the given {@link ProcessingUnit}.
     * 
     * @param unit the {@link ProcessingUnit}
     */
    private void updateProcessStatus(ProcessingUnit<E> unit) {
        Process process = unit.getProcess();
        this.getSession().refresh(process, LockMode.UPGRADE);
        process.notifyProcessingUnitFinished(unit);
        process.getPersister().save();

    }

    /**
     * @param mphase an {@link ExecPhase} definition
     * @return the {@link ExecPhase} instance to be used
     */
    @SuppressWarnings("unchecked")
    private ExecPhase<E> getExecPhases(MPhase mphase) {
        return this.controller.getExecPhaseFactory().createPhase(mphase);
    }

    /**
     * Executes the {@link ExecPhase} on all the {@link PhaseableElement} of the given {@link ProcessingUnit}.
     * 
     * @param unit {@link ProcessingUnit}
     * @param mphase {@link MPhase}
     * @param phase {@link ExecPhase}
     */
    private void executePhase(ProcessingUnit<E> unit, MPhase mphase, ExecPhase<E> phase) {
        for (E element : unit.getChildren()) {
            PhaseInstance<E> phaseInstance = this.getPhaseInstance(element, mphase);

            // we don't need to re-execute previously succesfully executed phases
            if (phaseInstance.getStatus().finished()) {
                this.throwEventDebug(JmineTecBatchMessages.PHASE_ALREADY_EXECUTED.create(element), element, unit, mphase);
                continue;
            }

            // checks if dependencies are satisfied
            if (!this.checkPhaseDependencies(mphase, element)) {
                this.updatePhaseInstanceStatus(phaseInstance, PhaseStatusEnum.ERROR);

                this.throwEventWarn(JmineTecBatchMessages.PHASE_SATISFIED_DEPENDENCIES.create(element), element, unit, mphase);
                continue;
            }

            if (phase.shouldExecute(element)) {
                this.throwEventDebug(JmineTecBatchMessages.PHASE_STARTED.create(element), element, unit, mphase);
                this.beforeElement(unit, phase, element);
                try {
                    this.updatePhaseInstanceStatus(phaseInstance, PhaseStatusEnum.STARTED);

                    this.executePhase(phase, element);

                    this.updatePhaseInstanceStatus(phaseInstance, PhaseStatusEnum.FINISHED);
                    this.throwEventDebug(JmineTecBatchMessages.PHASE_FINISHED.create(element), element, unit, mphase);
                } catch (Throwable e) {
                    if (this.isRelevant(e)) {
                        this.controller.getRtmController().monitor(e);
                        this.throwEventError(JmineTecBatchMessages.ERROR_RUNNIG_PHASE_RTM.create(), element, unit, mphase);
                    }
                    this.updatePhaseInstanceStatus(phaseInstance, PhaseStatusEnum.ERROR);
                    for (LocalizedMessageHolder messageHolder : this.userFriendlyExceptionInformer.getUserFriendlyMessages(e)) {
                        this.throwEventError(JmineTecBatchMessages.ERROR_RUNNIG_PHASE.create(element, messageHolder.getMessage()),
                                element, unit, mphase);
                    }
                }
                this.afterElement(unit, phase, element);
            } else {
                this.updatePhaseInstanceStatus(phaseInstance, PhaseStatusEnum.FINISHED);
            }
        }
    }

    /**
     * Effectively executes the {@link ExecPhase}.
     * 
     * @param phase {@link ExecPhase}
     * @param element {@link PhaseableElement}
     * @throws Exception in case an error occurs during the execution
     */
    private void executePhase(final ExecPhase<E> phase, final E element) throws Exception {
        E reloadedElement = this.executeTransacted(new BatchExecutionContextAwareCallable<E>(this.controller) {
            @Override
            protected E doCall() throws Exception {
                // locks the element prior to execution so it gets properly bound to the current session
                // note that only Cascade.ALL and Cascade.LOCK properties/collections of the element will be re-attached.
                E reloadedElement = AbstractPhaseManager.this.reloadElement(element);

                try {
                    getPhaseTimer().start(phase.getClass().getName());
                    phase.execute(reloadedElement);
                } finally {
                    getPhaseTimer().stop(phase.getClass().getName());
                }

                return reloadedElement;
            }
        });

        // updates the version of the element to account for phases that modify it
        element.setVersion(reloadedElement.getVersion());
    }

    /**
     * Returns a list of {@link ExecPhase} ordered so that dependencies are respected.
     * 
     * @param mproc {@link MProcess}
     * @return a list of all {@link ExecPhase} that should be executed for the given {@link MProcess}
     */
    private List<MPhase> getPhases(final MProcess mproc) {
        final List<MPhase> phases = new LinkedList<MPhase>();

        this.executeTransacted(new Callable<Void>() {
            public Void call() {
                MPhaseDAO mPhaseDAO = AbstractPhaseManager.this.controller.getDAOFactory().getDAOByClass(MPhaseDAO.class);
                List<MPhase> mPhases = mPhaseDAO.findByMProcess(AbstractPhaseManager.this.mprocess);
                phases.addAll(new PhaseGraph().addAll(mPhases).getOrderedPhases());
                return null;
            }
        });

        return phases;
    }

    /**
     * Returns the {@link PhaseInstance} for the given {@link PhaseableElement} and {@link MPhase}.
     * 
     * @param element the {@link PhaseableElement}
     * @param mphase the {@link MPhase}
     * @return the {@link PhaseInstance} for the given arguments
     */
    private PhaseInstance<E> getPhaseInstance(E element, MPhase mphase) {
        Map<MPhase, PhaseInstance<E>> map = this.phaseInstanceIndex.get(element);
        if (map == null) {
            throw new PhaseRuntimeException(JmineTecBatchMessages.PHASE_PHASE_INSTANCE_NOT_FOUND.create(mphase, element));
        } else {
            PhaseInstance<E> phaseInstance = map.get(mphase);
            if (phaseInstance == null) {
                throw new PhaseRuntimeException(JmineTecBatchMessages.PHASE_PHASE_INSTANCE_NOT_FOUND.create(mphase, element));
            }
            return phaseInstance;
        }
    }

    /**
     * @param element a {@link PhaseableElement}
     * @return <code>true</code> if all phases were executed successfully for the given {@link PhaseableElement}
     */
    protected final boolean isElementSucessfullyExecuted(E element) {
        Map<MPhase, PhaseInstance<E>> map = this.phaseInstanceIndex.get(element);
        if (map == null) {
            throw new PhaseRuntimeException(JmineTecBatchMessages.PHASE_PHASE_INSTANCE_NOT_FOUND.create(element));
        }

        for (PhaseInstance<E> phaseInstance : map.values()) {
            if (!phaseInstance.getStatus().finished()) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param mphase {@link MPhase}
     * @param element {@link PhaseableElement}
     * @return <code>true</code> if all dependencies of this {@link MPhase} were successfully executed for the given
     * {@link PhaseableElement}
     */
    protected boolean checkPhaseDependencies(MPhase mphase, E element) {
        for (MPhase mphaseDep : mphase.getDependencies()) {
            PhaseInstance<E> phaseInstance = this.getPhaseInstance(element, mphaseDep);
            if (!phaseInstance.getStatus().finished()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Updates the given {@link PhaseInstance} status.
     * 
     * @param phaseInstance {@link PhaseInstance}
     * @param status {@link PhaseStatusEnum}
     */
    private void updatePhaseInstanceStatus(final PhaseInstance<E> phaseInstance, final PhaseStatusEnum status) {
        this.executeTransacted(new Callable<Void>() {
            public Void call() {
                phaseInstance.setStatus(status);
                phaseInstance.getPersister().save();
                return null;
            }
        });
    }

    /**
     * @return the current {@link Session} (if any)
     */
    private Session getSession() {
        return this.controller.getSessionFactory().getCurrentSession();
    }

    /**
     * Returns a copy from database of the given {@link PhaseableElement}. Performs a version check to ensure that the
     * {@link PhaseableElement} has not been modified outside of this process.
     * 
     * @param element the {@link PhaseableElement}
     * @return the copy of the {@link PhaseableElement}
     */
    @SuppressWarnings("unchecked")
    protected final E reloadElement(E element) {
        Long oldVersion = element.getVersion() == null ? 0 : element.getVersion();
        Serializable pk = ((Persistable) element).getPk();
        E copy = (E) this.getSession().get(HibernateProxyHelper.getClassWithoutInitializingProxy(element), pk);
        Long newVersion = copy.getVersion();
        if (!oldVersion.equals(newVersion)) {
            throw new StaleObjectStateException(this.getSession().getEntityName(copy), pk);
        }
        return copy;
    }

    /**
     * @param unit the {@link ProcessingUnit}
     * @param phase the {@link ExecPhase}
     * @param element the {@link PhaseableElement}
     */
    private void beforeElement(final ProcessingUnit<E> unit, final ExecPhase<E> phase, final E element) {
        this.executeTransacted(new Callable<Void>() {
            public Void call() {
                AbstractPhaseManager.this.doBeforeElement(unit, phase, element);
                return null;
            }
        });
    }

    /**
     * Hook to add behavior before the execution of a given {@link ExecPhase} on a given {@link PhaseableElement}.
     * 
     * @param unit the {@link ProcessingUnit}
     * @param phase the {@link ExecPhase}
     * @param element the {@link PhaseableElement}
     */
    protected void doBeforeElement(ProcessingUnit<E> unit, ExecPhase<E> phase, E element) {
    }

    /**
     * @param unit the {@link ProcessingUnit}
     * @param phase the {@link ExecPhase}
     * @param element the {@link PhaseableElement}
     */
    private void afterElement(final ProcessingUnit<E> unit, final ExecPhase<E> phase, final E element) {
        this.executeTransacted(new BatchExecutionContextAwareCallable<Void>(this.controller) {
            @Override
            protected Void doCall() throws Exception {
                AbstractPhaseManager.this.doAfterElement(unit, phase, element);
                return null;
            }
        });
    }

    /**
     * Hook to add behavior after the execution of a given {@link ExecPhase} on a given {@link PhaseableElement}.
     * 
     * @param unit the {@link ProcessingUnit}
     * @param phase the {@link ExecPhase}
     * @param element the {@link PhaseableElement}
     */
    protected void doAfterElement(ProcessingUnit<E> unit, ExecPhase<E> phase, E element) {
    }

    /**
     * Marks this {@link ProcessingUnit} as started. Marks the related {@link Process} as started (if not already).
     * 
     * @param unit {@link ProcessingUnit}
     */
    private void beforeUnit(final ProcessingUnit<E> unit) {
        // updates processing unit status and process status
        this.executeTransacted(new Callable<Void>() {
            public Void call() {
                unit.setStatus(ProcessingUnit.STARETD);
                unit.setStartTime(new Timestamp());
                unit.getPersister().save();

                Process process = unit.getProcess();
                AbstractPhaseManager.this.controller.getSessionFactory().getCurrentSession().refresh(process, LockMode.UPGRADE);
                if (process.getStatus() == Process.NOT_STARETD) {
                    process.setStatus(Process.STARETD);
                    process.getPersister().save();
                }
                return null;
            }
        });

        this.executeTransacted(new BatchExecutionContextAwareCallable<Void>(this.controller) {
            @Override
            protected Void doCall() throws Exception {
                AbstractPhaseManager.this.doBeforeUnit(unit);
                return null;
            }
        });
    }

    /**
     * Hook to add behavior before a {@link ProcessingUnit} is executed.
     * 
     * @param unit {@link ProcessingUnit}
     */
    protected void doBeforeUnit(ProcessingUnit<E> unit) {
    }

    /**
     * Marks the related {@link Process} as FINISHED or ERROR if all related {@link ProcessingUnit} were already executed.
     * 
     * @param unit {@link ProcessingUnit}
     */
    private void afterUnit(final ProcessingUnit<E> unit) {
        this.executeTransacted(new BatchExecutionContextAwareCallable<Void>(this.controller) {
            @Override
            protected Void doCall() throws Exception {
                Process process = unit.getProcess();

                AbstractPhaseManager.this.controller.getSessionFactory().getCurrentSession().refresh(process, LockMode.UPGRADE);

                if (process.getQtdSucessUnits() + process.getQtdUnsucessUnits() == process.getQtdUnits()) {
                    if (process.getQtdUnsucessUnits() > 0) {
                        process.setStatus(Process.ERROR);
                    } else {
                        process.setStatus(Process.FINISHED);
                    }
                    process.setFinishTime(new Timestamp());
                    process.getPersister().save();
                }
                return null;
            }
        });

        this.executeTransacted(new BatchExecutionContextAwareCallable<Void>(this.controller) {
            @Override
            protected Void doCall() throws Exception {
                AbstractPhaseManager.this.doAfterUnit(unit);
                return null;
            }
        });
    }

    /**
     * Hook to add behavior after a {@link ProcessingUnit} is executed.
     * 
     * @param unit {@link ProcessingUnit}
     */
    protected void doAfterUnit(ProcessingUnit<E> unit) {
    }

    /**
     * @param unit {@link ProcessingUnit}
     * @param phase {@link ExecPhase}
     */
    private void beforePhase(final ProcessingUnit<E> unit, final ExecPhase<E> phase) {
        this.executeTransacted(new BatchExecutionContextAwareCallable<Void>(this.controller) {
            @Override
            protected Void doCall() throws Exception {
                AbstractPhaseManager.this.doBeforePhase(unit, phase);
                return null;
            }
        });
    }

    /**
     * Hook to add behavior before the execution of a given {@link ExecPhase}.
     * 
     * @param unit {@link ProcessingUnit}
     * @param phase {@link ExecPhase}
     */
    protected void doBeforePhase(ProcessingUnit<E> unit, ExecPhase<E> phase) {
    }

    /**
     * @param unit {@link ProcessingUnit}
     * @param phase {@link ExecPhase}
     */
    private void afterPhase(final ProcessingUnit<E> unit, final ExecPhase<E> phase) {
        this.executeTransacted(new BatchExecutionContextAwareCallable<Void>(this.controller) {
            @Override
            protected Void doCall() throws Exception {
                AbstractPhaseManager.this.doAfterPhase(unit, phase);
                return null;
            }
        });
    }

    /**
     * Hook to add behavior after the execution of a given {@link ExecPhase}.
     * 
     * @param unit {@link ProcessingUnit}
     * @param phase {@link ExecPhase}
     */
    protected void doAfterPhase(ProcessingUnit<E> unit, ExecPhase<E> phase) {
    }

    /**
     * Initializes the {@link PhaseInstance} index for the given {@link ProcessingUnit} elements and list of {@link MPhase}. The index will
     * contain an entry for each pair of {@link PhaseableElement} contained in the {@link ProcessingUnit} and {@link MPhase}. If none was
     * retrieved from the database for a given pair, one is created.
     * <p>
     * This will later allow us to check for previously successfully executed {@link ExecPhase} for a given {@link PhaseableElement} and
     * {@link MPhase} allowing us to skip them.
     * 
     * @param unit {@link ProcessingUnit}
     * @param phases list of {@link MPhase}
     */
    private void createPhaseInstances(final ProcessingUnit<E> unit, final List<MPhase> phases) {
        this.phaseInstanceIndex =
                this.executeTransacted(new BatchExecutionContextAwareCallable<Map<E, Map<MPhase, PhaseInstance<E>>>>(this.controller) {

                    @Override
                    protected Map<E, Map<MPhase, PhaseInstance<E>>> doCall() throws Exception {
                        List<? extends PhaseInstance<E>> instances = AbstractPhaseManager.this.getAllPhaseInstances(unit, phases);
                        Map<E, Map<MPhase, PhaseInstance<E>>> index = this.index(instances);

                        for (MPhase phase : phases) {
                            for (E element : unit.getChildren()) {
                                if (this.safeGet(index, element).get(phase) == null) {
                                    PhaseInstance<E> instance = AbstractPhaseManager.this.createPhaseInstance(element, phase);
                                    instance.getPersister().save();
                                    this.safeGet(index, element).put(phase, instance);
                                }
                            }
                        }
                        return index;
                    }

                    private Map<E, Map<MPhase, PhaseInstance<E>>> index(List<? extends PhaseInstance<E>> instances) {
                        Map<E, Map<MPhase, PhaseInstance<E>>> index = new HashMap<E, Map<MPhase, PhaseInstance<E>>>();
                        for (PhaseInstance<E> instance : instances) {
                            this.safeGet(index, instance.getElement()).put(instance.getPhase(), instance);
                        }
                        return index;
                    }

                    private Map<MPhase, PhaseInstance<E>> safeGet(Map<E, Map<MPhase, PhaseInstance<E>>> index, E element) {
                        Map<MPhase, PhaseInstance<E>> elementIndex = index.get(element);
                        if (elementIndex == null) {
                            elementIndex = new HashMap<MPhase, PhaseInstance<E>>();
                            index.put(element, elementIndex);
                        }
                        return elementIndex;
                    }
                });
    }

    /**
     * It is expected from this method to return the list of existing {@link PhaseInstance} for the given arguments.
     * 
     * @param unit {@link ProcessingUnit}
     * @param phases list of {@link MPhase}
     * @return a list of {@link PhaseInstance} for the given arguments
     */
    protected abstract List<? extends PhaseInstance<E>> getAllPhaseInstances(ProcessingUnit<E> unit, List<MPhase> phases);

    /**
     * @param element {@link PhaseableElement}
     * @param phase {@link ExecPhase}
     * @return the {@link PhaseInstance} for the given arguments
     */
    private PhaseInstance<E> createPhaseInstance(E element, MPhase phase) {
        PhaseInstance<E> inst = this.doCreatePhaseInstance(element);
        inst.setPhase(phase);
        inst.setStatus(PhaseStatusEnum.OPENED);
        return inst;
    }

    /**
     * It is expected from this method to return a valid {@link PhaseInstance} for the given {@link PhaseableElement}.
     * 
     * @param element {@link PhaseableElement}
     * @return {@link PhaseInstance}
     */
    protected abstract PhaseInstance<E> doCreatePhaseInstance(E element);

    /**
     * Executes the given {@link Callable} within a new database transaction.
     * 
     * @param action a {@link Callable}
     * @param <T> the type returned by the given {@link Callable}
     * @return the object returned from the {@link Callable}
     */
    public <T> T executeTransacted(Callable<T> action) {
        try {
            return this.controller.executeTransacted(action);
        } catch (Exception e) {
            throw new PersistenceException(e);
        }
    }

    /**
     * @param element the element
     * @param unit {@link ProcessingUnit}
     * @return event the event
     */
    protected abstract Event createEmptyEvent(E element, ProcessingUnit<E> unit);

    /**
     * @param level {@link EventLevelEnum}
     * @param menssageHolder event description
     * @param element element
     * @param unit {@link ProcessingUnit}
     * @param mphase {@link MPhase}
     */
    protected void throwEvent(EventLevelEnum level, LocalizedMessageHolder menssageHolder, E element, ProcessingUnit<E> unit, MPhase mphase) {
        Event event = this.createEmptyEvent(element, unit);
        event.acquireContextData(this.controller.getBatchExecutionContext());
        event.setActionMode(EventActionModeEnum.DISPATCH);
        event.setLevel(level);
        event.setDescr(menssageHolder.getMessage());
        event.setMnemonico(mphase.getMnemonico());
        this.controller.throwEvent(event);
    }

    /**
     * @param menssageHolder event description
     * @param element element
     * @param unit {@link ProcessingUnit}
     * @param mphase {@link MPhase}
     */
    protected void throwEventError(LocalizedMessageHolder menssageHolder, E element, ProcessingUnit<E> unit, MPhase mphase) {
        this.throwEvent(EventLevelEnum.ERROR, menssageHolder, element, unit, mphase);
    }

    /**
     * @param menssageHolder event description
     * @param element element
     * @param unit {@link ProcessingUnit}
     * @param mphase {@link MPhase}
     */
    protected void throwEventWarn(LocalizedMessageHolder menssageHolder, E element, ProcessingUnit<E> unit, MPhase mphase) {
        this.throwEvent(EventLevelEnum.WARN, menssageHolder, element, unit, mphase);
    }

    /**
     * @param menssageHolder event description
     * @param element element
     * @param unit {@link ProcessingUnit}
     * @param mphase {@link MPhase}
     */
    protected void throwEventInfo(LocalizedMessageHolder menssageHolder, E element, ProcessingUnit<E> unit, MPhase mphase) {
        this.throwEvent(EventLevelEnum.INFO, menssageHolder, element, unit, mphase);
    }

    /**
     * @param menssageHolder event description
     * @param element element
     * @param unit {@link ProcessingUnit}
     * @param mphase {@link MPhase}
     */
    protected void throwEventDebug(LocalizedMessageHolder menssageHolder, E element, ProcessingUnit<E> unit, MPhase mphase) {
        this.throwEvent(EventLevelEnum.DEBUG, menssageHolder, element, unit, mphase);
    }

    /**
     * Checks if this exception is relevant and should be logged.
     * 
     * @param e e
     * @return boolean
     */
    protected boolean isRelevant(Throwable e) {
        Throwable[] chain = ExceptionUtils.getThrowables(e);
        return !(chain[chain.length - 1] instanceof BusinessException);
    }
}
